package pl.edu.pw.elka.tinyrepo.client;

import java.io.InputStream;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.EnumMap;
import java.util.Map;
import java.util.StringTokenizer;

import pl.edu.pw.elka.tinyrepo.client.common.FakeResponse;
import pl.edu.pw.elka.tinyrepo.common.HTTPHeaderType;
import pl.edu.pw.elka.tinyrepo.common.Message;
import pl.edu.pw.elka.tinyrepo.common.Request;
import pl.edu.pw.elka.tinyrepo.common.Response;
import pl.edu.pw.elka.tinyrepo.common.StatusCode;
import pl.edu.pw.elka.tinyrepo.common.WebDAVMethod;

/**
 * Reprezentuje połaczenie aplikacji klienckiej z bramą 
 * 
 * @author Anna Stępień
 * @version 09-12-2011
 *
 */

public class Client
{
	/** Adres bramy */
	private final String gateAddress;
	/** Port bramy */
	private final int gatePort;
	/** Mapa przechowująca handlery do odpowiednich odpowiedzi bramy */
	private final Map<StatusCode, GateResponseHandler> handlersMap = 
			new EnumMap<StatusCode, GateResponseHandler>(StatusCode.class);
	
	/**
	 * Konstruktor klienta
	 * 
	 * @param gateAddress adres bramy
	 * @param gatePort port bramy
	 */
	public Client(final String gateAddress, final int gatePort)
	{
		this.gateAddress = gateAddress;
		this.gatePort = gatePort;
		
		registerGateResponseHandler(StatusCode.OK, new OKGateResponseHandler());
		registerGateResponseHandler(StatusCode.CREATED, new CREATEDGateResponseHandler());
		registerGateResponseHandler(StatusCode.NOT_FOUND, new NOTFOUNDGateResponseHandler());
		registerGateResponseHandler(StatusCode.FORBIDDEN, new FORBIDDENGateResponseHandler());
		registerGateResponseHandler(StatusCode.CONFLICT, new CONFLICTGateResponseHandler());
		registerGateResponseHandler(StatusCode.MOVED_PERMANENTLY, new REDIRECTGateResponseHandler());
		registerGateResponseHandler(StatusCode.NOT_IMPLEMENTED, new NOTIMPLEMENTEDGateResponseHandler());
		registerGateResponseHandler(StatusCode.NOT_SUPPORTED, new NOTSUPPORTEDGateResponseHandler());
		registerGateResponseHandler(StatusCode.BAD_REQUEST, new BADREQUESTGateResponseHandler());
	}
	
	/**
	 * Zwraca adres bramy
	 * @return adres bramy
	 * 
	 */
	final String getGateAddress()
	{
		return gateAddress;
	}
	
	/**
	 * Zwraca numer portu bramy
	 * @return numer portu
	 * 
	 */
	final int gatePort()
	{
		return gatePort;
	}
	
	/**
	 * Zapytanie get do bramy 
	 * 
	 * @param path względna ścieżka
	 * 
	 */
	public FakeResponse get(final String path)
	{
		Request request = new Request(WebDAVMethod.GET, path);
		
		FakeResponse gateResponse = serverRequest(gateAddress, gatePort, request, null);

		FakeResponse response = handlersMap.get(gateResponse.getStatusCode()).handle(gateResponse, request);
		return response;
	}
	
	/**
	 * Zapytanie PUT do bramy 
	 * 
	 * @param destinationPath względna ścieżka
	 * @param fileName nazwa pliku 
	 * 
	 */
	public FakeResponse put(final String destinationPath, Long size, InputStream data)
	{	
		Request request = new Request(WebDAVMethod.PUT, destinationPath); // FIXED dest zamiast fileName
		request.addHeader(HTTPHeaderType.CONTENT_LENGTH, size.toString());
		FakeResponse gateResponse = serverRequest(gateAddress, gatePort, request, null); // jezeli wiemy ze dane nie beda wyslane, mozna wstawic za data = null
		//FIXME UWAGA - funkcja dziala poprawnie, jezeli pierwszy response jest na pewno do gate
		// jezeli klient kontaktuje sie bezposrednio z serwerem danych, to nic do tego serwera nie 
		// zostanie wyslane - bo w linijce powyzej zostal wstawiony null
		// null jest dlatego wstawiony, bo przy pierwszym wywolaniu severRequest strumien
		// zostanie ustawiony na koniec - przy drugim serverRequest w handlerze REDIRECT
		// trzeba by strumien danych na poczatek, a tak poza tym jest to usprawnienie wydajnosciowe
		// TODO mozna to obejsc tworzac FakeRequest a i tam trzymac strukture File
		GateResponseHandler handler = handlersMap.get(gateResponse.getStatusCode());
		if(handler.getClass() == REDIRECTGateResponseHandler.class) { 
		   // redirect
		   ((REDIRECTGateResponseHandler)handler).setData(data, size); // przekaz strumien danych, bo moze byc potrzebny w redirect
		}
	    FakeResponse response = handler.handle(gateResponse, request);		
	    return response;
	}
	
	/**
	 * Zapytanie DELETE do bramy
	 * 
	 * @param path względna ścieżka
	 * 
	 */
	public FakeResponse delete(final String path)
	{	
		Request request = new Request(WebDAVMethod.DELETE, path);
		FakeResponse gateResponse = serverRequest(gateAddress, gatePort, request, null);
		
		FakeResponse response = handlersMap.get(gateResponse.getStatusCode()).handle(gateResponse, request);
		return response;		
	}
	
	
	/**
	 * Zapytanie COPY do bramy
	 * 
	 * @param sourcePath Względna ścieżka źródłowa
	 * @param destinationPath Względna ścieżka docelowa
	 */
	public FakeResponse copy(final String sourcePath, final String destinationPath)
	{
		Request request = new Request(WebDAVMethod.COPY, sourcePath);
		request.addHeader(HTTPHeaderType.DESTINATION, destinationPath);
		FakeResponse gateResponse = serverRequest(gateAddress, gatePort, request, null);

		FakeResponse response = handlersMap.get(gateResponse.getStatusCode()).handle(gateResponse, request);
		return response;		
	}
	
	/**
	 * Zapytanie MOVE do bramy
	 * 
	 * @param sourcePath Względna ścieżka źródłowa
	 * @param destinationPath Względna ścieżka docelowa
	 */
	public FakeResponse move(final String sourcePath, final String destinationPath)
	{
		Request request = new Request(WebDAVMethod.MOVE, sourcePath);
		request.addHeader(HTTPHeaderType.DESTINATION, destinationPath);
		FakeResponse gateResponse = serverRequest(gateAddress, gatePort, request, null);

		FakeResponse response = handlersMap.get(gateResponse.getStatusCode()).handle(gateResponse, request);
		return response;
	}
	
	/**
	 * Zapytanie MKCOL do bramy
	 * @param path Względna ścieżka
	 * 
	 */
	public FakeResponse mkcol(final String path)
	{
		Request request = new Request(WebDAVMethod.MKCOL, path);
		FakeResponse gateResponse = serverRequest(gateAddress, gatePort, request, null);

		FakeResponse response = handlersMap.get(gateResponse.getStatusCode()).handle(gateResponse, request);
		return response;		
	}
	
	/**
	 * Zapytanie PROPFIND do bramy
	 * @param path Względna ścieżka 
	 * 
	 */
	public FakeResponse propfind(final String path)
	{
		Request request = new Request(WebDAVMethod.PROPFIND, path);
		FakeResponse gateResponse = serverRequest(gateAddress, gatePort, request, null);

		FakeResponse response = handlersMap.get(gateResponse.getStatusCode()).handle(gateResponse, request);
		return response;		
	}
	
	/**
	 * Pomocnicza funkcja realizująca zapytanie do serwera (bramy lub serwera danych)
	 * 
	 * @param serverAddress adres serwera
	 * @param serverPort port serwera
	 * @param request zapytanie
	 * @param data - strumien danych do wyslania, jezeli null to znaczy ze brak danych
	 * @return odpowiedź serwera
	 * 
	 */
	private FakeResponse serverRequest(final String serverAddress, final int serverPort, final Request request, InputStream data)
	{
		Socket socket = null;
		InputStream input = null;
		FakeResponse response = null;
		
		try
		{
			socket = new Socket(serverAddress, serverPort);
			
			//wysłanie request do serwera
			request.write(socket.getOutputStream());
			System.out.println("Sent: "+request);
			// wyslanie danych zwiazanych z requestem
			
			//odczytanie response od serwera
			input = socket.getInputStream();
			Response serverResponse = new Response();
			serverResponse.setFirstLine(Message.readInputLine(input));
			serverResponse.readHeaders(input);
			System.out.println("Received: "+serverResponse);
			if(data != null && serverResponse.getStatusCode() == StatusCode.OK)
         {
            System.out.println("Sending data...");
            new SendHandler(data, socket.getOutputStream()).run(); // FIXME na razie wysylanie danych odbywa sie w glownym watku
         }
			//utwórz FakeResponse
			response = new FakeResponse(serverResponse.getStatusCode(), serverResponse.getHeaders(), input);
			
		}
		catch(UnknownHostException e)
		{
			e.printStackTrace();
		}
		catch(Exception e)
		{
			e.printStackTrace();
		}
		return response;		
	}
	
	private void registerGateResponseHandler(final StatusCode statusCode, 
			final GateResponseHandler handler)
	{
		handlersMap.put(statusCode, handler);
	}
	
	private class REDIRECTGateResponseHandler extends GateResponseHandler
	{
	   private InputStream data;
	   private Long size;
		@Override
		public FakeResponse handle(FakeResponse gateResponse, Request request)
		{
			//wyłuskaj adres i docelowy port oraz id

			   String location = gateResponse.getHeader(HTTPHeaderType.LOCATION);
	         StringTokenizer t = new StringTokenizer(location, "/");
	         String host = t.nextToken(); 
	         // WAZNE: w przypadku GET i DELETE w RESPONSE Location znajduje sie: adres:port/id
	         // zas w przypadku PUT  w response Location: znajduje sie tylko adres:port
	         // w Requescie PUT od klienta znajduje sie destination (sciezka do pliku w calym repo)
	         String id = null;
	         if(request.getMethod() == WebDAVMethod.GET || request.getMethod() == WebDAVMethod.DELETE ||
	               request.getMethod() == WebDAVMethod.PROPFIND) {
	            id = t.nextToken();
	         }
	         else if(request.getMethod() == WebDAVMethod.PUT) {
	            id = request.getPath(); // w requescie PUT znajduje sie sciezka do pliku w calym repo
	         }
	         
	         // host sklada sie z adresu serwera i numeru portu - trzeba to jeszcze rozdzielic
	         
	         StringTokenizer t2 = new StringTokenizer(host, ":");
	         String address = t2.nextToken();
	         String port = t2.nextToken();
	        
	        int serverPort = Integer.parseInt(port);
	        
	        //przygotuj nowy request
	        Request dataServerRequest = new Request(request.getMethod(), id);
	        if(request.getMethod() == WebDAVMethod.PUT)
	           dataServerRequest.addHeader(HTTPHeaderType.CONTENT_LENGTH, size.toString());
	        
	        // DELETE po redirect powinno zawierac jeszcze location - oryginalna sciezke do pliku w repo
	        if(request.getMethod() == WebDAVMethod.DELETE) 
	           dataServerRequest.addHeader(HTTPHeaderType.LOCATION, request.getPath());
	        
	        FakeResponse res = serverRequest(address, serverPort, dataServerRequest, data);
	        data = null; // wyzeruj referencje na strumien danych, nie jest juz wiecej potrzebny
	        size = 0L;
			return res;
		}
		
		/**
		 * Sluzy do ustawiania strumienia wyjsciowego dla redirect response
		 * @param data
		 */
		public void setData(InputStream data, Long size) {
		   this.data = data;
		   this.size = size;
		}
		
	}
	
	private class OKGateResponseHandler extends GateResponseHandler
	{

		@Override
		public FakeResponse handle(FakeResponse gateResponse, Request request)
		{
			return gateResponse;
		}
		
	}
	
	private class CREATEDGateResponseHandler extends GateResponseHandler
   {

      @Override
      public FakeResponse handle(FakeResponse gateResponse, Request request)
      {
         return gateResponse;
      }
      
   }
	
	private class FORBIDDENGateResponseHandler extends GateResponseHandler
   {

      @Override
      public FakeResponse handle(FakeResponse gateResponse, Request request)
      {
         return gateResponse;
      }
      
   }
	
	private class CONFLICTGateResponseHandler extends GateResponseHandler
   {

      @Override
      public FakeResponse handle(FakeResponse gateResponse, Request request)
      {
         return gateResponse;
      }
      
   }
	
	private class NOTFOUNDGateResponseHandler extends GateResponseHandler
	{

		@Override
		public FakeResponse handle(FakeResponse gateResponse, Request request)
		{
			return gateResponse;
		}
		
	}
	
	private class NOTIMPLEMENTEDGateResponseHandler extends GateResponseHandler
	{

		@Override
		public FakeResponse handle(FakeResponse gateResponse, Request request)
		{
			return gateResponse;
		}
		
	}
	
	private class NOTSUPPORTEDGateResponseHandler extends GateResponseHandler
	{

		@Override
		public FakeResponse handle(FakeResponse gateResponse, Request request)
		{
			return gateResponse;
		}
		
	}
	private class BADREQUESTGateResponseHandler extends GateResponseHandler
   {

      @Override
      public FakeResponse handle(FakeResponse gateResponse, Request request)
      {
         return gateResponse;
      }
      
   }
}
